import fg from "fast-glob";
import fs from "fs-extra";
import path from "path";
import { Project, ReferenceFindableNode } from "ts-morph";
import xml2js from "xml-js";
import { readGB2312, writeGB2312 } from "./vendor.js";
import moment from "moment";
import { toolchain } from "../toolchain.js";

interface IConvertItem {
    BinFile: string
    EntryMapFile: string
    ExcelFile: string
    IncludeSheet: string
    Meta: string
    Name: string
    Sort: 'Asc'
}

interface IResNode {
    _attributes: IConvertItem
}

interface IConvertList {
    ConvList: {
        ConvTree: {
            CommNode: {
                ResNode?: IResNode | IResNode[]
            }[]
        }
    }
}

declare type TConvertBook = { [itf: string]: IConvertItem };

export class JsonStriper {
    public async removeUseless(): Promise<void> {
        const project = new Project({
            tsConfigFilePath: path.join(toolchain.opts.projectRoot, 'TsScripts/tsconfig.json')
        });

        const outputFile = `tmp/${toolchain.opts.projectName}.jsonNotUsed.txt`;
        await this.findNotUsed(project, outputFile);

        const outContent = await fs.readFile(outputFile, 'utf-8');
        const lines = outContent.split(/\r?\n/);
        const notUsedItfs: string[] = [];
        const notUsedPpts: string[] = [];
        for (const line of lines) {
            if (!line || line.includes('---')) continue;
            const pair = line.split(':');
            if (pair.length == 1) {
                notUsedItfs.push(line);
            } else {
                notUsedPpts.push(line);
            }
        }

        const book = await this.loadConvertXml();
        await this.removeBin(book, notUsedItfs);
        await this.removeItfDefinitions(book, notUsedItfs, notUsedPpts);
        await this.removeJsonLoading();
    }

    private async findNotUsed(project: Project, outFile: string): Promise<void> {
        if (!toolchain.opts.designerRoot) {
            console.error('no designer root');
            process.exit(1);
        }
        // GameConfig.d.ts
        const usedItfs: string[] = [];
        const notUsedItfs: string[] = [];
        const notUsedPpts: string[] = [];
        const gameConfigFile = path.join(toolchain.opts.projectRoot, 'TsScripts/System/data/GameConfig.d.ts');
        const gameConfigSrc = project.getSourceFile(gameConfigFile)!;
        const module = gameConfigSrc.getModuleOrThrow('GameConfig');
        const interfaces = module.getInterfaces();
        for (const itf of interfaces) {
            const properties = itf.getProperties();
            const nups: string[] = [];
            for (const property of properties) {
                // if (property.getName() == 'm_iSkillPriority' && itf.getName() == 'SkillConfigM') {
                //     console.log('this comes!');
                // }
                if (!this.isNodeReferedOutside(property, [gameConfigFile])) {
                    nups.push(itf.getName() + ':' + property.getName());
                    // console.log('property not used:', itf.getName() + ':' + property.getName());
                }
            }
            if (nups.length == properties.length) {
                notUsedItfs.push(itf.getName());
            } else {
                usedItfs.push(itf.getName());
                notUsedPpts.push(...nups);
            }
        }
        // console.log('used itfs:', usedItfs.join(' | '));
        await fs.writeFile(outFile, notUsedItfs.join('\n') + '\n\n-------------\n\n' + notUsedPpts.join('\n'), 'utf-8');
        // 删除json文件
        for (const itf of notUsedItfs) {
            const jsonFile = path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/data', itf + '.json');
            if (fs.existsSync(jsonFile)) {
                await fs.unlink(jsonFile);
            }
            const metaFile = jsonFile + '.meta';
            if (fs.existsSync(metaFile)) {
                await fs.unlink(metaFile);
            }
        }
    }

    private isNodeReferedOutside(node: ReferenceFindableNode, excludes: string[]): boolean {
        const refSymbols = node.findReferences();
        let used = false;
        for (const refSymbol of refSymbols) {
            for (const ref of refSymbol.getReferences()) {
                if (ref.isDefinition()) continue;
                // console.log(`${refSymbol.getDefinition().getName()} refered in ${ref.getSourceFile().getFilePath()}`);
                if (!excludes.includes(path.resolve(ref.getSourceFile().getFilePath()))) {
                    used = true;
                    break;
                }
            }
            if (used) break;
        }
        return used;
    }

    private async loadConvertXml(): Promise<TConvertBook> {
        const book: TConvertBook = {};
        const convertXml = path.join(toolchain.opts.designerRoot, 'xml/1_ConvList.xml');
        const xmlContent = await readGB2312(convertXml);
        const xml = xml2js.xml2json(xmlContent, { compact: true, spaces: 0 });
        const xmlObj = JSON.parse(xml) as IConvertList;
        for (const node of xmlObj.ConvList.ConvTree.CommNode) {
            if (node.ResNode == null) continue;
            if (node.ResNode instanceof Array) {
                for (const n of node.ResNode) {
                    book[n._attributes.Meta.replace('_Flash', 'M')] = n._attributes;
                }
            } else {
                book[node.ResNode._attributes.Meta.replace('_Flash', 'M')] = node.ResNode._attributes;
            }
        }
        return book;
    }

    /**删除bin文件 */
    private async removeBin(book: TConvertBook, notUsedItfs: string[]): Promise<void> {
        for (const itf of notUsedItfs) {
            if (book[itf] == null) continue;
            const binFile = path.join(toolchain.opts.designerRoot, 'bin', book[itf].BinFile);
            if (fs.existsSync(binFile)) {
                await fs.unlink(binFile);
            }
            const xmlFile = binFile.replace('.bin', '.xml');
            if (fs.existsSync(xmlFile)) {
                await fs.unlink(xmlFile);
            }
        }
    }

    private async removeJsonLoading(): Promise<void> {
        const cmf = path.join(toolchain.opts.projectRoot, 'TsScripts/System/data/configmanager.ts');
        const cmfContent = await fs.readFile(cmf, 'utf-8');
        const lines = cmfContent.split(/\r?\n/);
        for (let i = lines.length - 1; i >= 0; i--) {
            const mch = lines[i].match(/^\s*(['|"])(\w+M)\1,?\s*$/);
            if (mch) {
                const jf = path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/data', mch[2] + '.json');
                if (!fs.existsSync(jf)) {
                    lines.splice(i, 1);
                }
            }
        }
        const newContent = lines.join('\n');
        if (newContent != cmfContent) {
            await fs.writeFile(cmf, newContent, 'utf-8');
        }
    }

    private async removeItfDefinitions(book: TConvertBook, notUsedItfs: string[], notUsedPpts: string[]): Promise<void> {
        const xmlRoot = path.join(toolchain.opts.designerRoot, 'xml');
        const xmlFiles = await fg('*.xml', { ignore: ['1_ConvList.xml'], cwd: xmlRoot });

        const svrStructs: string[] = [];
        for (const n in book) {
            if (book[n].BinFile.startsWith('Server\\')) {
                svrStructs.push(n);
            }
        }

        const notUsedPptMap: { [itf: string]: string[] } = {}, proteceds: string[] = [];
        for (const ppt of notUsedPpts) {
            const [itf, property] = ppt.split(':');
            let arr = notUsedPptMap[itf];
            if (arr == null) notUsedPptMap[itf] = arr = [];
            arr.push(property);
        }

        for (const x of xmlFiles) {
            const xmlFile = path.join(xmlRoot, x);
            const xmlContent = await readGB2312(xmlFile);
            const lines = xmlContent.split(/\r?\n/);

            // 先获取被服务器结构引用的子结构
            this.getProtectedStructures(lines, svrStructs, proteceds);
        }

        for (const x of xmlFiles) {
            const xmlFile = path.join(xmlRoot, x);
            const xmlContent = await readGB2312(xmlFile);
            const lines = xmlContent.split(/\r?\n/);
            
            // 获取被refer的字段
            const referedMap = this.getReferedEntries(lines);

            let theItf = '', sortkey = '', notUsed = false, newLines: string[] = [], modified = false, lastIsComment = false;
            for (let i = 0, len = lines.length; i < len; i++) {
                let line = lines[i];
                const originalLine = line;
                let removed = false;
                const mch = line.match(/^\s*<struct\s+name\s*=\s*"(\w+)"/);
                if (mch != null) {
                    // struct定义开始
                    theItf = mch[1].replace('_Flash', 'M');
                    const skmch = line.match(/sortkey\s*=\s*"(\w+)"/);
                    if (skmch) {
                        sortkey = skmch[1];
                    } else {
                        sortkey = '';
                    }
                    if (notUsedItfs.includes(theItf) && !proteceds.includes(theItf)) {
                        // 整个结构不要
                        notUsed = true;
                        removed = true;
                        if (lastIsComment) {
                            // 把注释也删掉
                            newLines.pop();
                        }
                    }
                } else {
                    if (notUsed) {
                        removed = true;
                    }
                    if (line.match(/^\s*<\/struct>/)) {
                        // struct定义结束
                        theItf = '';
                        sortkey = '';
                        notUsed = false;
                    } else if (theItf && !notUsed) {
                        const pmch = line.match(/^\s*<entry\s+name="(\w+)"/);
                        if (pmch != null) {
                            // entry定义开始
                            const entry = pmch[1];
                            const arr = notUsedPptMap[theItf];
                            if (arr != null && arr.includes(entry)) {
                                if (entry == sortkey || referedMap[theItf][entry] && !arr.includes(referedMap[theItf][entry]) || proteceds.includes(theItf)) {
                                    // sortkey、被refer的字段、和后台共享的子结构的字段均需保留，但添加convertOption noJson
                                    const coMch = line.match(/convertOption="(\S+)"/);
                                    if (coMch != null) {
                                        if (!coMch[1].includes('noJson')) {
                                            line = line.replace('convertOption="', 'convertOption="noJson,').replace('?', '');
                                        }
                                    } else {
                                        line = line.replace('/>', ' convertOption="noJson"/>');
                                    }
                                } else {
                                    // 字段不要
                                    removed = true;
                                }
                            }
                        } 
                    }
                }
                if (removed) {
                    modified = true;
                } else {
                    if (line != originalLine) modified = true;
                    newLines.push(line);
                }
                lastIsComment = line.match(/^\s*<!--.+-->\s*$/) != null;
            }
            if (modified) await writeGB2312(xmlFile, newLines.join('\n'));
        }
    }

    private getProtectedStructures(lines: string[], svrStructs: string[], out: string[]): void {
        // 获取因被服务器结构引用而需要保护的结构
        let isSvrStruct = false;
        for (let i = 0, len = lines.length; i < len; i++) {
            let line = lines[i];
            const mch = line.match(/^\s*<struct\s+name\s*=\s*"(\w+?)"/);
            if (mch != null) {
                // struct定义开始
                const structName = mch[1];
                isSvrStruct = svrStructs.includes(structName) || structName.endsWith('_Server');
            } else {
                if (line.match(/^\s*<\/struct>/)) {
                    // struct定义结束
                    isSvrStruct = false;
                } else if (isSvrStruct) {
                    const pmch = line.match(/^\s*<entry\s+.*\btype\b="(\w+)"/);
                    if (pmch != null) {
                        // entry定义开始
                        const type = pmch[1];
                        if (!out.includes(type)) out.push(type);
                    } 
                }
            }
        }
    }

    private getReferedEntries(lines: string[]): { [itf: string]: { [refered: string]: string } } {
        // 获取因被refer而需要保护的字段
        const out: { [itf: string]: { [refered: string]: string } } = {};
        let theItf = '';
        for (let i = 0, len = lines.length; i < len; i++) {
            let line = lines[i];
            const mch = line.match(/^\s*<struct\s+name\s*=\s*"(\w+)".*/);
            if (mch != null) {
                // struct定义开始
                theItf = mch[1].replace('_Flash', 'M');
                out[theItf] = {}
            } else {
                if (line.match(/^\s*<\/struct>/)) {
                    // struct定义结束
                    theItf = '';
                } else if (theItf) {
                    const pmch = line.match(/^\s*<entry\s+.*\bname\b\s*=\s*"(\w+)".+refer\s*="(\w+)"/);
                    if (pmch != null) {
                        // entry定义开始
                        const entry = pmch[1], refer = pmch[2];
                        out[theItf][refer] = entry;
                    } 
                }
            }
        }
        return out;
    }

    public async checkLiveness(): Promise<void> {
        const cfgs = toolchain.opts.cfg?.jsonCfg?.lives;
        if (!cfgs) {
            console.error('no liveness configuration.');
            process.exit(1);
        }

        const outputFile = `tmp/${toolchain.opts.projectName}.jsonDead.txt`;
        await fs.ensureFile(outputFile);
        await fs.appendFile(outputFile, '-------------------------------\n' + moment().format(moment.HTML5_FMT.DATETIME_LOCAL_SECONDS) + '\n\n', 'utf-8');

        const jsonRoot = path.join(toolchain.opts.projectRoot, 'Assets/AssetSources/data');
        for (const c of cfgs) {
            const file = path.join(jsonRoot, c.file);
            const f: any[] = await fs.readJSON(file) as any[];
            const content = await fs.readFile(file, 'utf-8');
            let values = f.map((v) => v[c.key]) as number[];
            const total = values.length;
            for (const rk of c.reserveKeys) {
                values = values.filter((v) => !content.includes(`"${rk}": ${v}`));
            }

            const otherJsons = await fg('*.json', { cwd: jsonRoot, ignore: [c.file] });
            for (const oj of otherJsons) {
                const otherContent = await fs.readFile(path.join(jsonRoot, oj), 'utf-8');
                values = values.filter((v) => !otherContent.includes(String(v)));
                if (values.length == 0) {
                    break;
                }
            }

            if (values.length > 0) {
                const sumary = `${c.file}, total cnt: ${total}, dead cnt: ${values.length}`;
                console.log(sumary);
                await fs.appendFile(outputFile, sumary + '\n' + values.join('\n') + '\n\n');
            }
        }
    }
}
